Skip to content

Buy Foundations through RevenueCat, and let entitlement expire - #602

Merged
maximsan merged 2 commits into
mainfrom
feat/revenuecat-purchase
Sep 13, 2026
Merged

maximsan merged 2 commits into
mainfrom
feat/revenuecat-purchase

Conversation

@maximsan

@maximsan maximsan commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Why

The app could not take money. This builds the store behind all three pricing
arms — #421, which stays open until the sandbox criteria can actually be run.
The ruling is ADR-0024,
which supersedes ADR-0003.

Watch for

  • None of this has met a real store. It is written against the
    purchases_flutter API and compiles, but no call has run against RevenueCat
    or StoreKit. Every judgement below is unverified until the account work in
    docs/10-payments.md is done.
  • Which plan an owner holds is asked of the store, through a new
    activeTerm() on the seam, rather than read from PurchasedTerm — which
    records only what this session bought, and so called a returning
    subscriber a one-time buyer. Purchases now draws nothing until the store has
    named the plan; a beat of blank beats a wrong plan. A SKU this build does
    not know reads as bought outright, which is the reading that offers nothing
    to cancel.
  • purchaseUpdates is gone, replaced by entitlementChanges. It had no
    consumer anywhere in lib/; the stream that was actually needed is the one
    that reports a lapse.
  • The Purchases body lives in features/monetization/, not profile/.
    The payments seam guard forbids anything outside the paywall layer reading
    which arm a learner is on, and this screen's copy changes per arm. The
    Settings file is the frame only.
  • Offline falls back to the last known answer. RevenueCat serves cached
    customer info, so a throw means it has never answered; guessing false
    there would lock out a paying learner.
  • GRANT_COURSE wins over REVENUECAT_KEY when both are passed, so a
    development build that asked for the owned state still gets it.

Not here

Checks

  • flutter test — 3197 pass, 15 new.
  • The expiry test was checked against a deliberately broken build first: with
    the change-stream watch removed it fails Expected: false, Actual: true.
  • Metrics, format, comment cap, guard tests and the changelog check all ran in
    the pre-push hook.
  • SPM resolves purchases-ios-spm 5.88.0, so the Podfile-free iOS setup
    survives the new dependency.

Implements the store behind all three pricing arms (#421). RevenueCat holds
the entitlement, prices the plans and says which arm a learner is on;
offeringFor stays the one home for what each arm sells.

Entitlement stops being a one-shot answer: courseEntitlement watches the
store's own change stream, so a subscription that lapses, is refunded or
fails to renew locks the course with no restart.

Settings -> Purchases becomes real: what you own and how it is billed, or
the arm's offer, Restore, and a link out to Apple for a renewing plan.

ADR-0024 supersedes ADR-0003. A build reaches a store only when given
REVENUECAT_KEY, so the shipped app stays free by construction.
An owner who restarts bought nothing this run, so reading PurchasedTerm
called a subscriber a one-time buyer and hid their way to cancel.

PaymentsService gains activeTerm(), answered from RevenueCat's active
entitlement and its productIdentifier. Purchases draws nothing until the
store has named the plan: a wrong plan is worse than a beat of blank.

PurchasedTerm stays what it was — the record of what this session bought,
which is what the celebration is about.
@maximsan
maximsan merged commit f19fe75 into main Sep 13, 2026
7 checks passed
@maximsan
maximsan deleted the feat/revenuecat-purchase branch September 13, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant